<html>
  <head>
    <style>
      body {
        background-color: #111;
        color: #ccc;
        font-family: sans-serif;
        font-size: 18px;
      }
      section {
        max-width: 900px;
        margin: 0 auto;
        padding: 0 24px;
      }
      h1 {
        margin: 32px auto;
      }
      li {
        margin: 8px;
      }
      code {
        color: #aaa;
        background-color: #000;
        padding: 1px 4px;
        font-size: 16px;
      }
      input,
      button {
        font-size: 20px;
        font-weight: bold;
        padding: 4px 12px;
      }
      select {
        font-size: 18px;
        padding: 4px 12px;
      }
      small {
        font-style: italic;
      }
      #error {
        color: #f00;
      }
      .controls {
        margin-top: 64px;
        display: flex;
        flex-direction: column;
      }
      .controls button {
        margin: 16px;
      }
      .row {
        display: flex;
        justify-content: center;
        align-items: center;
        margin-bottom: 24px;
      }
      .row-file label {
        margin: 0 32px;
      }
    </style>
  </head>
  <body>
    <section>
      <h1>Debug Wasm tests in the browser!</h1>
      <p>
        You can step through the generated code instruction-by-instruction, and
        examine memory contents
      </p>
      <h3>Steps</h3>
      <ul>
        <li>
          Run
          <code
            >ROC_WRITE_FINAL_WASM=1 cargo test-gen-wasm
            my_test_function_name</code
          >
        </li>
        <li>
          Look for the path written to the console for
          <code>final.wasm</code> and select it in the file picker below
        </li>
        <li>
          Open the browser DevTools <br />
          <small> Control+Shift+I or Command+Option+I or F12 </small>
        </li>
        <li>
          The debugger should pause just before entering the first Wasm call.
          Step into a couple of Wasm calls until you reach your test code in
          <code>$main</code>
        </li>
        <li>
          Chrome DevTools has a handy Memory Inspector panel. In the debugger,
          find <code>Module -> memories -> $memory</code>. Right click and
          select "Reveal in Memory Inspector"
        </li>
      </ul>

      <div class="controls">
        <div class="row">
          <label>
            <input type="checkbox" id="refcount-test" />
            Check this box if your test is in <code>gen_refcount.rs</code>
          </label>
        </div>
        <div class="row row-file">
          <label for="wasm-file">Select final.wasm</label>
          <input id="wasm-file" type="file" />
        </div>
        <div id="error" class="row"></div>
        <div class="row">
          <button id="button-run">RUN</button>
        </div>
      </div>
    </section>
    <script>
      const file_input = document.getElementById("wasm-file");
      const refcount_checkbox = document.getElementById("refcount-test");
      const button = document.getElementById("button-run");
      const error_box = document.getElementById("error");

      if (localStorage.getItem("refcount-test")) {
        refcount_checkbox.checked = true;
      }
      refcount_checkbox.onchange = function (ev) {
        if (ev.target.checked) {
          localStorage.setItem("refcount-test", "true");
        } else {
          localStorage.removeItem("refcount-test");
        }
      };

      button.onclick = function () {
        if (refcount_checkbox.checked) {
          runRefcountTest();
        } else {
          runExpressionTest();
        }
      };

      file_input.onchange = function () {
        error_box.innerHTML = "";
      };

      async function runExpressionTest() {
        const file = getFile();
        const instance = await compileFileToInstance(file);

        debugger; // Next call is Wasm! Step into test_wrapper, then $main
        instance.exports.test_wrapper();
      }

      async function runRefcountTest() {
        const file = getFile();
        const instance = await compileFileToInstance(file);
        const MAX_ALLOCATIONS = 100;
        const refcount_vector_addr =
          instance.exports.init_refcount_test(MAX_ALLOCATIONS);

        debugger; // Next call is Wasm! Step into test_wrapper, then $main
        instance.exports.test_wrapper();

        const words = new Uint32Array(instance.exports.memory.buffer);
        function deref(addr8) {
          return words[addr8 >> 2];
        }

        const actual_len = deref(refcount_vector_addr);
        const rc_pointers = [];
        for (let i = 0; i < actual_len; i++) {
          const offset = (1 + i) << 2;
          const rc_ptr = deref(refcount_vector_addr + offset);
          rc_pointers.push(rc_ptr);
        }

        const rc_encoded = rc_pointers.map((ptr) => ptr && deref(ptr));
        const rc_encoded_hex = rc_encoded.map((x) =>
          x ? x.to_string(16) : "(deallocated)"
        );
        const rc_values = rc_encoded.map((x) => x && x - 0x80000000 + 1);

        console.log({ rc_values, rc_encoded_hex });
      }

      function getFile() {
        const { files } = file_input;
        if (!files.length) {
          const msg = "Select a file!";
          error_box.innerHTML = msg;
          throw new Error(msg);
        }
        return files[0];
      }

      async function compileFileToInstance(file) {
        const buffer = await file.arrayBuffer();

        const wasiLinkObject = {};
        const importObject = createFakeWasiImports(wasiLinkObject);
        const result = await WebAssembly.instantiate(buffer, importObject);

        wasiLinkObject.memory8 = new Uint8Array(
          result.instance.exports.memory.buffer
        );
        wasiLinkObject.memory32 = new Uint32Array(
          result.instance.exports.memory.buffer
        );

        return result.instance;
      }

      // If you print to stdout (for example in the platform), it calls these WASI imports.
      // This implementation uses console.log
      function createFakeWasiImports(wasiLinkObject) {
        const decoder = new TextDecoder();

        // fd_close : (i32) -> i32
        // Close a file descriptor. Note: This is similar to close in POSIX.
        // https://docs.rs/wasi/latest/wasi/wasi_snapshot_preview1/fn.fd_close.html
        function fd_close(fd) {
          console.warn(`fd_close: ${{ fd }}`);
          return 0; // error code
        }

        // fd_fdstat_get : (i32, i32) -> i32
        // Get the attributes of a file descriptor.
        // https://docs.rs/wasi/latest/wasi/wasi_snapshot_preview1/fn.fd_fdstat_get.html
        function fd_fdstat_get(fd, stat_mut_ptr) {
          /*
            Tell WASI that stdout is a tty (no seek or tell)

            https://github.com/WebAssembly/wasi-libc/blob/659ff414560721b1660a19685110e484a081c3d4/libc-bottom-half/sources/isatty.c

            *Not* a tty if:
                (statbuf.fs_filetype != __WASI_FILETYPE_CHARACTER_DEVICE ||
                  (statbuf.fs_rights_base & (__WASI_RIGHTS_FD_SEEK | __WASI_RIGHTS_FD_TELL)) != 0)

            So it's sufficient to set:
              .fs_filetype = __WASI_FILETYPE_CHARACTER_DEVICE
              .fs_rights_base = 0

            https://github.com/WebAssembly/wasi-libc/blob/659ff414560721b1660a19685110e484a081c3d4/libc-bottom-half/headers/public/wasi/api.h

                typedef uint8_t __wasi_filetype_t;
                typedef uint16_t __wasi_fdflags_t;
                typedef uint64_t __wasi_rights_t;
                #define __WASI_FILETYPE_CHARACTER_DEVICE (UINT8_C(2))
                typedef struct __wasi_fdstat_t { // 24 bytes total
                    __wasi_filetype_t fs_filetype;        // 1 byte
                                                          // 1 byte padding
                    __wasi_fdflags_t fs_flags;            // 2 bytes
                                                          // 4 bytes padding
                    __wasi_rights_t fs_rights_base;       // 8 bytes
                    __wasi_rights_t fs_rights_inheriting; // 8 bytes
                } __wasi_fdstat_t;
          */
          // console.warn(`fd_fdstat_get: ${{ fd, stat_mut_ptr }}`);
          const WASI_FILETYPE_CHARACTER_DEVICE = 2;
          wasiLinkObject.memory8[stat_mut_ptr] = WASI_FILETYPE_CHARACTER_DEVICE;
          wasiLinkObject.memory8
            .slice(stat_mut_ptr + 1, stat_mut_ptr + 24)
            .fill(0);

          return 0; // error code
        }

        // fd_seek : (i32, i64, i32, i32) -> i32
        // Move the offset of a file descriptor. Note: This is similar to lseek in POSIX.
        // https://docs.rs/wasi/latest/wasi/wasi_snapshot_preview1/fn.fd_seek.html
        function fd_seek(fd, offset, whence, newoffset_mut_ptr) {
          console.warn(`fd_seek: ${{ fd, offset, whence, newoffset_mut_ptr }}`);
          return 0;
        }

        // fd_write : (i32, i32, i32, i32) -> i32
        // Write to a file descriptor. Note: This is similar to `writev` in POSIX.
        // https://docs.rs/wasi/latest/wasi/wasi_snapshot_preview1/fn.fd_write.html
        function fd_write(fd, iovs_ptr, iovs_len, nwritten_mut_ptr) {
          let string_buffer = "";
          let nwritten = 0;
          const STDOUT = 1;

          for (let i = 0; i < iovs_len; i++) {
            const index32 = iovs_ptr >> 2;
            const base = wasiLinkObject.memory32[index32];
            const len = wasiLinkObject.memory32[index32 + 1];
            iovs_ptr += 8;

            if (!len) continue;

            nwritten += len;

            // For some reason we often get negative-looking buffer lengths with junk data.
            // Just skip the console.log, but still increase nwritten or it will loop forever.
            // Dunno why this happens, but it's working fine for printf debugging ¯\_(ツ)_/¯
            if (len >> 31) {
              break;
            }

            const buf = wasiLinkObject.memory8.slice(base, base + len);
            const chunk = decoder.decode(buf);
            string_buffer += chunk;
          }
          wasiLinkObject.memory32[nwritten_mut_ptr >> 2] = nwritten;
          if (string_buffer) {
            if (fd === STDOUT) {
              console.log(string_buffer);
            } else {
              console.error(string_buffer);
            }
          }
          return 0;
        }

        // proc_exit : (i32) -> nil
        function proc_exit(exit_code) {
          throw new Error(`Wasm exited with code ${exit_code}`);
        }

        // send_panic_msg_to_rust (i32, i32) => {}
        function send_panic_msg_to_rust(msg, length) {
          throw new Error("Wasm hit a panic");
        }

        // Signatures from wasm_test_platform.o
        const sig2 = (i32) => {};
        const sig6 = (i32a, i32b) => 0;
        const sig7 = (i32a, i32b, i32c) => 0;
        const sig9 = (i32a, i64b, i32c) => 0;
        const sig10 = (i32a, i64b, i64c, i32d) => 0;
        const sig11 = (i32a, i64b, i64c) => 0;
        const sig12 = (i32a) => 0;
        const sig13 = (i32a, i64b) => 0;
        const sig14 = (i32a, i32b, i32c, i64d, i32e) => 0;
        const sig15 = (i32a, i32b, i32c, i32d) => 0;
        const sig16 = (i32a, i64b, i32c, i32d) => 0;
        const sig17 = (i32a, i32b, i32c, i32d, i32e) => 0;
        const sig18 = (i32a, i32b, i32c, i32d, i64e, i64f, i32g) => 0;
        const sig19 = (i32a, i32b, i32c, i32d, i32e, i32f, i32g) => 0;
        const sig20 = (i32a, i32b, i32c, i32d, i32e, i64f, i64g, i32h, i32i) =>
          0;
        const sig21 = (i32a, i32b, i32c, i32d, i32e, i32f) => 0;
        const sig22 = () => 0;

        return {
          env: {
            send_panic_msg_to_rust,
          },
          wasi_snapshot_preview1: {
            args_get: sig6,
            args_sizes_get: sig6,
            environ_get: sig6,
            environ_sizes_get: sig6,
            clock_res_get: sig6,
            clock_time_get: sig9,
            fd_advise: sig10,
            fd_allocate: sig11,
            fd_close,
            fd_datasync: sig12,
            fd_fdstat_get,
            fd_fdstat_set_flags: sig6,
            fd_fdstat_set_rights: sig11,
            fd_filestat_get: sig6,
            fd_filestat_set_size: sig13,
            fd_filestat_set_times: sig10,
            fd_pread: sig14,
            fd_prestat_get: sig6,
            fd_prestat_dir_name: sig7,
            fd_pwrite: sig14,
            fd_read: sig15,
            fd_readdir: sig14,
            fd_renumber: sig6,
            fd_seek,
            fd_sync: sig12,
            fd_tell: sig6,
            fd_write,
            path_create_directory: sig7,
            path_filestat_get: sig17,
            path_filestat_set_times: sig18,
            path_link: sig19,
            path_open: sig20,
            path_readlink: sig21,
            path_remove_directory: sig7,
            path_rename: sig21,
            path_symlink: sig17,
            path_unlink_file: sig7,
            poll_oneoff: sig15,
            proc_exit,
            proc_raise: sig12,
            sched_yield: sig22,
            random_get: sig6,
            sock_recv: sig21,
            sock_send: sig17,
            sock_shutdown: sig6,
          },
        };
      }
    </script>
  </body>
</html>
